using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Roslyn.Services;
using Roslyn.Compilers;
using Roslyn.Compilers.Common;
using Roslyn.Compilers.CSharp;

namespace Multiplicity
{
    public static class Extensions
    {
        /// <summary>
        /// Removes all multiplicity attributes out of the syntay list.
        /// </summary>
        /// <param name="pAttributeList">The attribute list.</param>
        /// <param name="pRootNode">The root node.</param>
        /// <param name="pTunIntoComment">if set to <c>true</c> [turn into comment].</param>
        /// <returns></returns>
        public static SyntaxNode RemoveMultiplicityAttribute(this SyntaxNode pRootNode, SyntaxList<AttributeListSyntax> pAttributeList, bool pTurnIntoComment = false)
        {
            var syntaxNode = pRootNode;
            if (pAttributeList.Count == 1)//case it's the only attribute
            {
                syntaxNode = syntaxNode.RemoveNodes(pAttributeList, SyntaxRemoveOptions.KeepExteriorTrivia);
                //add att as leading comment trivia to the node
                if (pTurnIntoComment)//TODO: does not yet work properly if other Attributes are within the same brackets
                {
                    syntaxNode = syntaxNode.AddMultiplicityAttributeNodeAsComment(pAttributeList[0]);
                }
            }
            else
            {
                var multiplicityAtt = pAttributeList.GetMultiplicityAttributes();

                foreach (AttributeSyntax att in multiplicityAtt)
                {
                    syntaxNode = syntaxNode.RemoveNode(att, SyntaxRemoveOptions.KeepExteriorTrivia);
                    //add att as leading comment trivia to the node
                    if (pTurnIntoComment) //TODO: does not yet work properly
                    {
                        syntaxNode = syntaxNode.AddMultiplicityAttributeNodeAsComment(att);
                    }
                }
            }
            return syntaxNode;
        }

        /// <summary>
        /// Adds the node as comment.
        /// </summary>
        /// <param name="pNode">The p node.</param>
        /// <param name="pNodeAsComment">The node as comment.</param>
        /// <returns></returns>
        public static SyntaxNode AddMultiplicityAttributeNodeAsComment(this SyntaxNode pNode, SyntaxNode pNodeAsComment)
        {
            SyntaxTrivia newComment;
            if (pNodeAsComment.Kind == SyntaxKind.AttributeList)
            {
                var attSynList = pNodeAsComment as AttributeListSyntax;
                if ((attSynList != null) && (attSynList.Attributes.Count > 1))
                {
                    AttributeSyntax multiAtt = null;
                    var sepList = new SeparatedSyntaxList<AttributeSyntax>();
                    foreach (var att in attSynList.Attributes)
                    {
                        if (MultiplicityAttribute.EqualsName(((IdentifierNameSyntax)(att.Name)).Identifier.ValueText))
                        {
                            multiAtt = att;
                            continue;
                        }
                        sepList = sepList.Add(att);
                    }
                    var attList = Syntax.AttributeList(sepList);

                    newComment = Syntax.SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia, attList.ToString() + (multiAtt != null ? "//" + multiAtt.ToString() : String.Empty) + Environment.NewLine);
                }
                else
                    newComment = Syntax.SyntaxTrivia(SyntaxKind.SingleLineCommentTrivia, "//" + pNodeAsComment.ToString() + Environment.NewLine);
            }
            else return pNode;

            var newTriviaList = pNode.GetLeadingTrivia().ToList();
            try
            {
                //find the last non whitespace trivia
                var lastNonWhitespace = newTriviaList.Last(x => (x.Kind != SyntaxKind.WhitespaceTrivia) && (x.Kind != SyntaxKind.EndOfLineTrivia));
                var lastIndex = newTriviaList.IndexOf(lastNonWhitespace);
                //keep a newline if exists
                if ((newTriviaList.Count > lastIndex + 1) &&
                    (newTriviaList[lastIndex + 1].Kind == SyntaxKind.EndOfLineTrivia))
                    lastIndex++;
                //remove unncessary whitespaces
                newTriviaList.RemoveRange(lastIndex + 1, newTriviaList.Count - lastIndex - 1);
            }
            catch (InvalidOperationException e)//newTriviaList.Last may fail
            { Debug.WriteLine(e.ToString()); }


            newTriviaList.Add(newComment);
            var triList = Syntax.TriviaList(newTriviaList);
            return pNode.WithLeadingTrivia(triList);
        }

        public static FieldDeclarationSyntax ChangeToMultiplicityType(this FieldDeclarationSyntax pFieldNode, MultiplicityKind pMultiKind, TypeSyntax pNewType)
        {
            var orgType = pFieldNode.Declaration.Type;
            orgType.CheckDestMultipliciyType(pMultiKind, pNewType);
            return pFieldNode.WithDeclaration(Syntax.VariableDeclaration(pNewType, pFieldNode.Declaration.Variables));
        }

        private static void CheckDestMultipliciyType(this TypeSyntax orgType, MultiplicityKind pMultiKind, TypeSyntax pNewType)
        {
            if (orgType.Kind == SyntaxKind.TypeVarKeyword) //no need to change anything (type inference)
                return;

            switch (pMultiKind)
            {
                case MultiplicityKind.Any:
                    var genType = pNewType as GenericNameSyntax;
                    if (genType == null)
                        throw new ArgumentException("MultiplicityKind.Any requires a generic type container.", "pNewType");
                    if (genType.TypeArgumentList.Arguments.Count != 1)
                        throw new ArgumentException("The generic type does only have one argument.", "pNewType");
                    var passedType = genType.TypeArgumentList.Arguments[0].ToString();
                    if (!passedType.Equals(orgType.ToString()))
                        throw new ArgumentException(String.Format("The generic type argument type does not match the original type. It's meant to be {0}, but it is {1}", orgType, passedType), "pNewType");
                    break;
                case MultiplicityKind.Optional:
                //var semType = SemModel.GetTypeInfo(pNewType);
                //if (!(semType.Type.IsReferenceType
                //    || (semType.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T))
                //    )                    
                //    throw new ArgumentException("MultiplicityKind.Optional requires a a nullable type.", "pNewType");
                //break;
                default: //Bare
                    return;
            }
        }

        public static PropertyDeclarationSyntax ChangeToMultiType(this PropertyDeclarationSyntax pPropNode, TypeSyntax pNewType)
        {
            var orgType = pPropNode.Type;
            var genType = pNewType as GenericNameSyntax;
            if (genType.TypeArgumentList.Arguments.Count != 1)
                throw new ArgumentException("The generic type does only have one argument.", "pNewType");
            var passedType = genType.TypeArgumentList.Arguments[0].ToString();
            if (!passedType.Equals(orgType.ToString()))
                throw new ArgumentException(String.Format("The generic type argument type does not match the original type. It's meant to be {0}, but it is {1}", orgType, passedType), "pNewType");
            return pPropNode.WithType(pNewType);
        }


        public static TypeSyntax GetMultiplicityType(this IEnumerable<AttributeSyntax> pAttributeSyntax)
        {
            foreach (var atts in pAttributeSyntax)
                foreach (var arg in atts.ArgumentList.Arguments)
                {
                    var argType = arg.Expression as TypeOfExpressionSyntax;
                    if (argType != null)
                        return argType.Type;

                }
            return null;
        }

        public static MultiplicityKind GetMultiplicityKind(this IEnumerable<AttributeSyntax> pAttributeSyntax)
        {
            foreach (var atts in pAttributeSyntax)
                foreach (var arg in atts.ArgumentList.Arguments)
                {
                    var argKind = arg.Expression as MemberAccessExpressionSyntax;
                    if ((argKind != null) && (argKind.GetFirstToken().ToString().Equals(typeof(MultiplicityKind).Name)))
                    {
                        string kind = argKind.GetLastToken().ToString();
                        switch (kind)
                        {
                            case "Any": return MultiplicityKind.Any;
                            case "Optional": return MultiplicityKind.Optional;
                            default: return MultiplicityKind.Bare;
                        }
                    }

                }
            return MultiplicityKind.Bare;
        }


        /// <summary>
        /// Gets the next node in the tree.
        /// </summary>
        /// <param name="pSyntaxNode">The syntax node.</param>
        /// <returns></returns>
        public static SyntaxNode GetNextNode(this SyntaxNode pSyntaxNode)
        {
            var childNodeList = pSyntaxNode.Parent.ChildNodes().ToList();
            for (int i = 0; i < childNodeList.Count; i++)
            {
                if (childNodeList[i].Equals(pSyntaxNode) && ((i + 1) < childNodeList.Count))
                    return childNodeList[i + 1];
            }
            return null;
        }

        /// <summary>
        /// Gets the multiplicity attributes out of the syntax list.
        /// </summary>
        /// <param name="pAttributeList">The attribute list.</param>
        /// <returns></returns>
        public static IEnumerable<AttributeSyntax> GetMultiplicityAttributes(this SyntaxList<AttributeListSyntax> pAttributeList)
        {
            return pAttributeList.SelectMany(a => a.ChildNodes().OfType<AttributeSyntax>())
                                                .Where(a => MultiplicityAttribute.EqualsName(((IdentifierNameSyntax)(a.Name)).Identifier.ValueText));
        }

        public static Stack<CommonSyntaxNode> FindAllReferences(this VariableDeclarationSyntax pNode, ISemanticModel pSemModel, ISolution pSolution, CommonSyntaxTree pTree)
        {
            //http://connect.microsoft.com/VisualStudio/feedback/details/759204/getdeclaredsymbol-no-longer-works-on-fielddeclarationsyntax-in-v1-1
            var syms = pNode.Variables.Select(v => pSemModel.GetDeclaredSymbol(v));

            return FindNodesForSymbols(pSolution, pTree, syms);
        }

        public static Stack<CommonSyntaxNode> FindAllReferences(this SyntaxNode pNode, ISemanticModel pSemModel, ISolution pSolution, CommonSyntaxTree pTree)
        {
            var sym = pSemModel.GetDeclaredSymbol(pNode);
            var syms = Enumerable.Repeat(sym, 1);

            return FindNodesForSymbols(pSolution, pTree, syms);
        }

        private static Stack<CommonSyntaxNode> FindNodesForSymbols(ISolution pSolution, CommonSyntaxTree pTree, IEnumerable<ISymbol> syms)
        {
            var resultStack = new Stack<CommonSyntaxNode>();
            
            foreach (var symbol in syms)
            {
                var findResult = symbol.FindReferences(pSolution);
                foreach (ReferencedSymbol reference in findResult)
                {
                    foreach (var loc in reference.Locations)
                    {
                        var position = loc.Location.SourceSpan.Start;
                        var token = loc.Location.SourceTree.GetRoot().FindToken(position);
                        var node = token.Parent;
                        //if (node.Parent != null)
                            resultStack.Push(node.Parent);
                        //else
                        //    resultStack.Push(node);
                    }
                }
            }

            return resultStack;
        }




    }
}
